1 /** 2 * This module implements functionality helpful for writing integration tests 3 * as opposed to the unit variety where unit-tests are defined as not 4 * having global side-effects. In constrast, this module implements 5 * assertions that check for global side-effects such as writing to the 6 * file system. 7 */ 8 9 module unit_threaded.integration; 10 11 version (Windows) { 12 extern (C) int mkdir(char*); 13 extern (C) char* mktemp(char* template_); 14 15 char* mkdtemp(char* t) { 16 version (unitUnthreaded) 17 return mkdtempImpl(t); 18 else { 19 synchronized { 20 return mkdtempImpl(t); 21 } 22 } 23 } 24 25 char* mkdtempImpl(char* t) { 26 char* result = mktemp(t); 27 28 if (result is null) 29 return null; 30 if (mkdir(result)) 31 return null; 32 33 return result; 34 } 35 36 } else { 37 extern (C) char* mkdtemp(char* template_); 38 } 39 40 shared static this() { 41 import std.file : exists, rmdirRecurse; 42 43 if (Sandbox.sandboxesPath.exists) 44 rmdirRecurse(Sandbox.sandboxesPath); 45 } 46 47 @safe: 48 49 /** 50 Responsible for creating a temporary directory to serve as a sandbox where 51 files can be created, written to or deleted. 52 */ 53 struct Sandbox { 54 import std.path; 55 56 enum defaultSandboxesPath = buildPath("tmp", "unit-threaded"); 57 static string sandboxesPath = defaultSandboxesPath; 58 string testPath; 59 60 /// Instantiate a Sandbox object 61 static Sandbox opCall() { 62 Sandbox ret; 63 ret.testPath = newTestDir; 64 return ret; 65 } 66 67 static void setPath(string path) { 68 import std.file : exists, mkdirRecurse; 69 70 sandboxesPath = path; 71 if (!sandboxesPath.exists) 72 () @trusted{ mkdirRecurse(sandboxesPath); }(); 73 } 74 75 static void resetPath() { 76 sandboxesPath = defaultSandboxesPath; 77 } 78 79 /// Write a file to the sandbox 80 void writeFile(in string fileName, in string output = "") const { 81 import std.stdio : File; 82 import std.path : buildPath, dirName; 83 import std.file : mkdirRecurse; 84 85 () @trusted{ mkdirRecurse(buildPath(testPath, fileName.dirName)); }(); 86 File(buildPath(testPath, fileName), "w").writeln(output); 87 } 88 89 /// Write a file to the sanbox 90 void writeFile(in string fileName, in string[] lines) const { 91 import std.array; 92 93 writeFile(fileName, lines.join("\n")); 94 } 95 96 /// Assert that a file exists in the sandbox 97 void shouldExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const { 98 import std.file : exists; 99 import std.path : buildPath; 100 import unit_threaded.should : fail; 101 102 fileName = buildPath(testPath, fileName); 103 if (!fileName.exists) 104 fail("Expected " ~ fileName ~ " to exist but it didn't", file, line); 105 } 106 107 /// Assert that a file does not exist in the sandbox 108 void shouldNotExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const { 109 import std.file : exists; 110 import std.path : buildPath; 111 import unit_threaded.should : fail; 112 113 fileName = buildPath(testPath, fileName); 114 if (fileName.exists) 115 fail("Expected " ~ fileName ~ " to not exist but it did", file, line); 116 } 117 118 /// read a file in the test sandbox and verify its contents 119 void shouldEqualContent(in string fileName, in string content, 120 in string file = __FILE__, in size_t line = __LINE__) const { 121 import std.file : readText; 122 import std..string : chomp, splitLines; 123 import unit_threaded.should : shouldEqual; 124 125 readText(buildPath(testPath, fileName)).shouldEqual(content, file, line); 126 } 127 128 /// read a file in the test sandbox and verify its contents 129 void shouldEqualLines(in string fileName, in string[] lines, 130 string file = __FILE__, size_t line = __LINE__) const { 131 import std.file : readText; 132 import std..string : chomp, splitLines; 133 import unit_threaded.should : shouldEqual; 134 135 readText(buildPath(testPath, fileName)).chomp.splitLines.shouldEqual(lines, file, line); 136 } 137 138 // `fileName` should contain `needle` 139 void fileShouldContain(in string fileName, in string needle, 140 in string file = __FILE__, in size_t line = __LINE__) { 141 import std.file : readText; 142 import unit_threaded.should : shouldBeIn; 143 144 needle.shouldBeIn(readText(inSandboxPath(fileName)), file, line); 145 } 146 147 string sandboxPath() @safe @nogc pure nothrow const { 148 return testPath; 149 } 150 151 string inSandboxPath(in string fileName) @safe pure nothrow const { 152 import std.path : buildPath; 153 154 return buildPath(sandboxPath, fileName); 155 } 156 157 /** 158 Executing `args` should succeed 159 */ 160 void shouldSucceed(string file = __FILE__, size_t line = __LINE__)(in string[] args...) @safe const { 161 import unit_threaded.should : UnitTestException; 162 import std.conv : text; 163 import std.array : join; 164 165 const res = executeInSandbox(args); 166 if (res.status != 0) 167 throw new UnitTestException(text("Could not execute `", 168 args.join(" "), "`:\n", res.output), file, line); 169 } 170 171 alias shouldExecuteOk = shouldSucceed; 172 173 /** 174 Executing `args` should fail 175 */ 176 void shouldFail(string file = __FILE__, size_t line = __LINE__)(in string[] args...) @safe const { 177 import unit_threaded.should : UnitTestException; 178 import std.conv : text; 179 import std.array : join; 180 181 const res = executeInSandbox(args); 182 if (res.status == 0) 183 throw new UnitTestException(text("`", args.join(" "), 184 "` should have failed but didn't:\n", res.output), file, line); 185 } 186 187 private: 188 189 auto executeInSandbox(in string[] args) @safe const { 190 import std.process : execute, Config; 191 import std.algorithm : startsWith; 192 import std.array : replace; 193 194 const string[string] env = null; 195 const config = Config.none; 196 const maxOutput = size_t.max; 197 const workDir = testPath; 198 199 const executable = args[0].startsWith("./") 200 ? inSandboxPath(args[0].replace("./", "")) : args[0]; 201 202 return execute(executable ~ args[1 .. $], env, config, maxOutput, workDir); 203 } 204 205 static string newTestDir() { 206 import std.file : exists, mkdirRecurse; 207 208 if (!sandboxesPath.exists) { 209 () @trusted{ mkdirRecurse(sandboxesPath); }(); 210 } 211 212 return makeTempDir(); 213 } 214 215 static string makeTempDir() { 216 import std.algorithm : copy; 217 import std.exception : enforce; 218 import std.conv : to; 219 import std..string : fromStringz; 220 import core.stdc..string : strerror; 221 import core.stdc.errno : errno; 222 223 char[2048] template_; 224 copy(buildPath(sandboxesPath, "XXXXXX") ~ '\0', template_[]); 225 226 auto path = () @trusted{ return mkdtemp(&template_[0]).to!string; }(); 227 228 enforce(path != "", 229 "\n" ~ "Failed to create temporary directory name using template '" ~ () @trusted{ 230 return fromStringz(&template_[0]); 231 }() ~ "': " ~ () @trusted{ return strerror(errno).to!string; }()); 232 233 return path.absolutePath; 234 } 235 }